In [1]:
# display plots inline
%matplotlib notebook

# imports
import os
import numpy as np
import pandas as pd
import pymc3 as pm
from bambi import Model, Prior
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats
import pymc3_utils as pmu

# suppress system warnings for legibility
import warnings
warnings.filterwarnings('ignore')

# resize plots to fit labels inside bounding box
from matplotlib import rcParams
rcParams.update({'figure.autolayout': True})

# MPI color scheme
sns.set(style='white', palette='Set2')
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/xarray/core/merge.py:17: FutureWarning: The Panel class is removed from pandas. Accessing it from the top-level namespace will also be removed in the next version
  PANDAS_TYPES = (pd.Series, pd.DataFrame, pd.Panel)
/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/statsmodels/tools/_testing.py:19: FutureWarning: pandas.util.testing is deprecated. Use the functions in the public API at pandas.testing instead.
  import pandas.util.testing as tm

Word reading

Loading data

In [2]:
df_reading = pd.read_csv('data/tamil_reading.tsv', sep='\t')
df_span = pd.read_csv('data/span_intercepts.tsv', sep='\t')[['pp', 'raw_span_mean', 'span_intercept']]
df_ravens = pd.read_csv('data/ravens_intercepts.tsv', sep='\t')[['pp', 'raw_ravens_mean', 'ravens_intercept']]
df_reading = df_reading.merge(df_ravens, left_on='pp', right_on='pp')
df_reading = df_reading.merge(df_span, left_on='pp', right_on='pp')
display(df_reading.head().round(2))
subject pp literate word pseudoword raw_ravens_mean ravens_intercept raw_span_mean span_intercept
0 1 low_1 low 24 1 0.28 0.34 4.5 0.47
1 2 y_2 y 63 42 0.67 1.77 5.0 0.69
2 3 y_3 y 80 49 0.56 1.38 5.5 0.91
3 4 y_4 y 47 33 0.39 0.78 4.5 0.47
4 5 n_5 n 0 0 0.06 -0.78 2.0 -0.64

Data mangling and plotting

We suspect reading scores are highly correlated with our cognitive ability measures (ravens and digit span). To get a better idea of these relationships, we will draw a heatmap of absolute (rectified) correlations.

In [3]:
# check correlations
corrs = df_reading[[
    'word', 'pseudoword',
    'raw_span_mean', 'span_intercept',
    'raw_ravens_mean', 'ravens_intercept']].corr().round(2)
display(corrs)
diag = np.eye(*corrs.shape)
diag[diag == 1] = np.nan
corrs = np.abs(corrs + diag)
g = sns.heatmap(corrs, cmap='magma', vmin=0, vmax=1, annot=True)
g.set_xticklabels(
    g.get_xticklabels(),
    rotation=45, ha='right')
g.set_yticklabels(
    g.get_yticklabels(),
    rotation=0)
g.set(ylim=(len(corrs), 0))
g.set_title('absolute correlations between predictors', pad=12)
plt.savefig('figures/precorrection_heatmap.pdf')
plt.savefig('figures/precorrection_heatmap.png', dpi=600, bbox_inches='tight')
word pseudoword raw_span_mean span_intercept raw_ravens_mean ravens_intercept
word 1.00 0.93 0.51 0.51 0.53 0.52
pseudoword 0.93 1.00 0.48 0.48 0.47 0.47
raw_span_mean 0.51 0.48 1.00 1.00 0.48 0.48
span_intercept 0.51 0.48 1.00 1.00 0.48 0.48
raw_ravens_mean 0.53 0.47 0.48 0.48 1.00 0.99
ravens_intercept 0.52 0.47 0.48 0.48 0.99 1.00

If we want to use reading score as a predictor of literacy, independent of cognitive ability (and other relevant factors that were likely unintentionally captured in the cognitive ability scores, such as familiarity with formal test-taking) we will need to correct the reading scores. Our correction procedure consists of regressing out common variance with the cognitive ability measures.

In [4]:
df_reading['literate'] = df_reading['literate'].replace({'y': 'literate', 'low': 'low-literate', 'n': 'illiterate'})
df_reading = pd.melt(df_reading, id_vars=['subject', 'pp', 'literate',
                                          'raw_ravens_mean', 'ravens_intercept',
                                          'raw_span_mean', 'span_intercept'],
                     value_vars=['word', 'pseudoword'], var_name='task', value_name='score')
display(df_reading.head().round(2))
subject pp literate raw_ravens_mean ravens_intercept raw_span_mean span_intercept task score
0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24
1 2 y_2 literate 0.67 1.77 5.0 0.69 word 63
2 3 y_3 literate 0.56 1.38 5.5 0.91 word 80
3 4 y_4 literate 0.39 0.78 4.5 0.47 word 47
4 5 n_5 illiterate 0.06 -0.78 2.0 -0.64 word 0

To confirm that the reading scores is indeed associated with literacy in a meaningful way, we are plotting the distribution of word and pseudoword reading scores in each self-reported literacy group (literate, low-literate, and illiterate).

In [5]:
# plot reading score distributions
g = sns.catplot(x='task', y='score', hue='literate',
                hue_order=['illiterate', 'low-literate', 'literate'],
                scale='count', cut=0, inner='quartile',
                kind='violin', data=df_reading, legend=False)
g.set(ylim=(0, 100), xlabel='', ylabel='number of words read correctly')
g.ax.legend(loc='upper right', frameon=False)
plt.savefig('figures/reading_scores.pdf')
plt.savefig('figures/reading_scores.png', dpi=600, bbox_inches='tight')

Self-reported literacy is strongly associated with word reading scores, but there is clearly overlap between the categories (i.e., some participants reported being low-literate, but scored better than other participants that reported being fully literate) which is likely due to participants finding it difficult to estimate their own literacy relative to others. We will therefore use the measured word reading scores as our measure of literacy in the analyses reported below.

In [6]:
# standardize variables
df_reading['task_z'] = pmu.standardize(pd.get_dummies(df_reading['task'])['word'])
df_reading['ravens_z'] = pmu.standardize(df_reading['raw_ravens_mean'])
df_reading['span_z'] = pmu.standardize(df_reading['raw_span_mean'])
display(df_reading.head().round(2))
subject pp literate raw_ravens_mean ravens_intercept raw_span_mean span_intercept task score task_z ravens_z span_z
0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24 1.0 0.39 0.81
1 2 y_2 literate 0.67 1.77 5.0 0.69 word 63 1.0 2.81 1.20
2 3 y_3 literate 0.56 1.38 5.5 0.91 word 80 1.0 2.12 1.58
3 4 y_4 literate 0.39 0.78 4.5 0.47 word 47 1.0 1.08 0.81
4 5 n_5 illiterate 0.06 -0.78 2.0 -0.64 word 0 1.0 -1.00 -1.12

Because word reading scores range from 0 to 100 (with hard boundaries) it would be incorrect to model them using a linear regression model. Instead we are modelling each word in the reading task as a Bernoulli trial, using a generalized linear (i.e., logistic) model. In order to make this possible, we are going to expand each participants' reading score into 100 trials.

In [7]:
# expand binomial dataset to bernoulli trials for modeling
df_bernoulli = pmu.expand_binomial(df_reading, 'score', 100)
display(df_bernoulli.head().round(2))
index subject pp literate raw_ravens_mean ravens_intercept raw_span_mean span_intercept task score task_z ravens_z span_z score_bernoulli
0 0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24 1.0 0.39 0.81 1
1 0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24 1.0 0.39 0.81 1
2 0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24 1.0 0.39 0.81 1
3 0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24 1.0 0.39 0.81 1
4 0 1 low_1 low-literate 0.28 0.34 4.5 0.47 word 24 1.0 0.39 0.81 1

Models

In [8]:
# default model params
defaults = {
    'samples': 5000,
    'tune': 2500,
    'chains': 4,
    'init': 'advi+adapt_diag',
    'family': 'bernoulli',
    'priors': {'fixed': 'narrow', 'random': 'narrow'},
}

We expect a model with task (word versus pseudoword), digit span, and ravens score, plus the interactions between digit span and task, and ravens score and task as predictors to fit best, but we will also compare models with subsets of those predictors to see if a more parsimonious model perhaps has just as good a fit.

In [9]:
fixed_task = Model(df_bernoulli)
fixed_task.fit('score_bernoulli ~ task_z',
               **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 10,961:  21%|██        | 10399/50000 [00:23<01:29, 440.02it/s]
Convergence achieved at 10400
Interrupted at 10,399 [20%]: Average Loss = 11,412
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [01:03<00:00, 471.63draws/s]
Out[9]:
<bambi.results.MCMCResults at 0x12e7d94e0>
In [10]:
fixed_task_span = Model(df_bernoulli)
fixed_task_span.fit('score_bernoulli ~ task_z + span_z',
                    **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 10,199:  20%|██        | 10099/50000 [00:24<01:38, 405.36it/s]
Convergence achieved at 10100
Interrupted at 10,099 [20%]: Average Loss = 10,934
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [span_z, task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [01:30<00:00, 331.41draws/s]
Out[10]:
<bambi.results.MCMCResults at 0x12ced4f60>
In [11]:
fixed_task_ravens = Model(df_bernoulli)
fixed_task_ravens.fit('score_bernoulli ~ task_z + ravens_z',
                      **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 10,321:  20%|██        | 10199/50000 [00:26<01:42, 387.70it/s]
Convergence achieved at 10200
Interrupted at 10,199 [20%]: Average Loss = 11,052
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [ravens_z, task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [02:19<00:00, 215.12draws/s]
Out[11]:
<bambi.results.MCMCResults at 0x12fc286a0>
In [12]:
fixed_task_span_ravens = Model(df_bernoulli)
fixed_task_span_ravens.fit('score_bernoulli ~ task_z + span_z + ravens_z',
                           **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 10,021:  22%|██▏       | 10899/50000 [00:47<02:49, 230.99it/s]
Convergence achieved at 10900
Interrupted at 10,899 [21%]: Average Loss = 10,907
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [ravens_z, span_z, task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [02:51<00:00, 175.34draws/s]
Out[12]:
<bambi.results.MCMCResults at 0x1330c6fd0>
In [13]:
fixed_taskxspanxravens = Model(df_bernoulli)
fixed_taskxspanxravens.fit('score_bernoulli ~ task_z*span_z + task_z*ravens_z',
                           **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 9,932.8:  41%|████▏     | 20699/50000 [01:25<02:00, 242.70it/s]
Convergence achieved at 20700
Interrupted at 20,699 [41%]: Average Loss = 10,643
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [task_z:ravens_z, ravens_z, task_z:span_z, span_z, task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [04:32<00:00, 110.13draws/s]
Out[13]:
<bambi.results.MCMCResults at 0x133053668>

Model comparison of fixed-effects models

In [14]:
from importlib import reload
reload(pmu)
g_comparison, comparison = pmu.compare({
    'task': fixed_task,
    'task + span': fixed_task_span,
    'task + ravens': fixed_task_ravens,
    'task + span + ravens': fixed_task_span_ravens,
    'task * span + task * ravens': fixed_taskxspanxravens,
}, ic='LOO')
display(comparison)
plt.savefig('figures/reading_model_comparison.pdf')
plt.savefig('figures/reading_model_comparison.png', dpi=600, bbox_inches='tight')
rank loo p_loo d_loo weight se dse warning loo_scale
task * span + task * ravens 0 -9914.84 6.35084 0 NaN 62.8159 0 False log
task + span + ravens 1 -9922.85 4.24022 8.00361 NaN 69.8689 4.48193 False log
task + span 2 -10111.9 3.22013 197.063 NaN 70.0543 20.8783 False log
task + ravens 3 -10230 3.1088 315.129 NaN 69.2911 25.5228 False log
task 4 -10903.9 2.00124 989.035 0 70.4314 42.4791 False log

As expected, the most complex model fits best, but interestingly, the reading_score ~ task + span + ravens model without interactions fits just as well (after correcting for model complexity) meaning that there is no meaningful interaction between task and the other two predictors. We will now refit this nicely parsimonious model with by-participant intercepts in order to be able to use these intercepts as corrected reading scores in our recognition memory models.

Best fit fixed effects model with random intercepts by participant

In [15]:
mixed_taskxspanxravens = Model(df_bernoulli)
mixed_taskxspanxravens.fit('score_bernoulli ~ task_z*span_z + task_z*ravens_z',
                      random=['1|pp'],
                      **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 7,946.5:  48%|████▊     | 23999/50000 [01:30<01:37, 266.41it/s]
Convergence achieved at 24000
Interrupted at 23,999 [47%]: Average Loss = 8,977.5
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [1|pp_offset, 1|pp_sd, task_z:ravens_z, ravens_z, task_z:span_z, span_z, task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [24:34<00:00, 20.34draws/s]
The number of effective samples is smaller than 10% for some parameters.
Out[15]:
<bambi.results.MCMCResults at 0x134c18ef0>
In [16]:
display(pm.summary(mixed_taskxspanxravens.backend.trace, credible_interval=.95).round(2))
mean sd hpd_2.5% hpd_97.5% mcse_mean mcse_sd ess_mean ess_sd ess_bulk ess_tail r_hat
Intercept[0] -2.13 0.21 -2.54 -1.71 0.01 0.01 730.0 730.0 729.0 1944.0 1.00
task_z[0] 0.47 0.02 0.43 0.52 0.00 0.00 17620.0 17620.0 17617.0 14116.0 1.00
span_z[0] 0.86 0.23 0.43 1.32 0.01 0.00 1094.0 1094.0 1095.0 2204.0 1.00
task_z:span_z[0] 0.01 0.03 -0.04 0.06 0.00 0.00 15016.0 12713.0 15008.0 14861.0 1.00
ravens_z[0] 0.68 0.22 0.26 1.11 0.01 0.00 1172.0 1172.0 1173.0 2216.0 1.01
... ... ... ... ... ... ... ... ... ... ... ...
1|pp[91] 1.56 0.27 1.04 2.08 0.01 0.01 998.0 998.0 997.0 2706.0 1.00
1|pp[92] 2.03 0.31 1.44 2.64 0.01 0.01 985.0 985.0 983.0 2605.0 1.00
1|pp[93] 1.42 0.44 0.55 2.25 0.01 0.01 1240.0 1240.0 1240.0 2439.0 1.00
1|pp[94] 0.93 0.30 0.37 1.53 0.01 0.01 986.0 986.0 985.0 2505.0 1.00
1|pp[95] 1.67 0.26 1.17 2.17 0.01 0.01 1061.0 1061.0 1062.0 3208.0 1.00

199 rows × 11 columns

NUTS warns that the number of effective samples is smaller than 10% for some parameters. This can be an indicator of several sampling issues, most likely mild autocorrelation. The sampling stats still show more than 1.5K effective samples for all parameters and $\hat{r}$ values also look good.
We will plot model traces below to look at the autocorrelation issue.

Plotting model traces

In [17]:
g_traces = pm.traceplot(mixed_taskxspanxravens.backend.trace)
plt.savefig('figures/reading_model_traces.png', dpi=600)

The posterior traces (right column) show signs of very mild autocorrelation, but nothing pathological. (We appear to be exploring the parameter space fairly well.)
Posterior densities (left column) also look well-behaved (i.e. unimodal, roughly normally distributed).

In [18]:
pps = df_reading['pp'].unique()
pp_nums = [f'1|pp[{i}]' for i in range(len(pps))]
df_intercepts = pm.summary(mixed_taskxspanxravens.backend.trace).loc[pp_nums]
df_intercepts['pp'] = np.sort(pps)

display(df_intercepts.head().round(2))
mean sd hpd_3% hpd_97% mcse_mean mcse_sd ess_mean ess_sd ess_bulk ess_tail r_hat pp
1|pp[0] -0.86 0.34 -1.51 -0.25 0.01 0.01 1264.0 1264.0 1264.0 3254.0 1.0 low_1
1|pp[1] 2.10 0.26 1.60 2.59 0.01 0.01 1002.0 1002.0 1001.0 3133.0 1.0 low_10
1|pp[2] 0.63 0.31 0.05 1.22 0.01 0.01 1016.0 1016.0 1014.0 2734.0 1.0 low_11
1|pp[3] 0.20 0.28 -0.32 0.74 0.01 0.01 1140.0 1140.0 1140.0 3087.0 1.0 low_12
1|pp[4] 0.30 0.44 -0.49 1.15 0.01 0.01 1027.0 1027.0 1026.0 2540.0 1.0 low_13
In [19]:
df_uncorrected = df_reading.groupby('pp', as_index=False).mean().rename(columns={'score': 'raw_reading_score'})
df_intercepts = df_intercepts[['pp', 'mean']].rename(columns={'mean': 'adjusted_reading_score'})
df_intercepts = df_intercepts.merge(df_uncorrected,
                                    left_on='pp', right_on='pp').reset_index()

display(df_intercepts.head().round(2))
index pp adjusted_reading_score subject raw_ravens_mean ravens_intercept raw_span_mean span_intercept raw_reading_score task_z ravens_z span_z
0 0 low_1 -0.86 1.0 0.28 0.34 4.5 0.47 12.5 0.0 0.39 0.81
1 1 low_10 2.10 40.0 0.17 -0.17 3.5 0.02 45.5 0.0 -0.31 0.04
2 2 low_11 0.63 41.0 0.22 0.10 4.5 0.47 32.5 0.0 0.04 0.81
3 3 low_12 0.20 42.0 0.28 0.34 4.0 0.25 22.5 0.0 0.39 0.43
4 4 low_13 0.30 43.0 0.11 -0.46 5.0 0.69 23.5 0.0 -0.65 1.20
In [20]:
# write intercepts to file
df_intercepts.to_csv('data/reading_intercepts.tsv', sep='\t')

To make sure our score correction had the desired effect, we plot a heatmap of absolute correlations between the cognitive ability measures and both the corrected and uncorrected reading scores.

In [21]:
# check correlations
corrs = df_intercepts[[
    'raw_reading_score',
    'raw_span_mean',
    'raw_ravens_mean',
    'adjusted_reading_score',
]].corr().round(2)
display(corrs)
upper = np.triu(np.ones(corrs.shape))
upper[upper == 1] = np.nan
corrs = np.abs(corrs + upper)
plt.figure(figsize=(5, 3.5))
g = sns.heatmap(corrs, cmap='magma', vmin=0, vmax=1, annot=True)
labels = [
    'raw reading score',
    'digit span',
    'raven\'s score',
    'adjusted reading score',
]
g.set_xticklabels(
    labels,
    rotation=30, ha='right')
g.set_yticklabels(
    labels,
    rotation=0)
g.set(ylim=(len(corrs), 1), xlim=(0, len(corrs) - 1))
g.set_title('absolute correlations between predictors', pad=12)
plt.savefig('figures/postcorrection_heatmap.pdf')
plt.savefig('figures/postcorrection_heatmap.png', dpi=600, bbox_inches='tight')
raw_reading_score raw_span_mean raw_ravens_mean adjusted_reading_score
raw_reading_score 1.00 0.51 0.51 0.72
raw_span_mean 0.51 1.00 0.48 0.04
raw_ravens_mean 0.51 0.48 1.00 0.04
adjusted_reading_score 0.72 0.04 0.04 1.00

Correlation between unadjusted reading and both cognitive ability measures was .51, after correction the correlation has been reduced to less than .05, while unadjusted and adjusted reading score still share around 50% of their variance (r = .71).

Reading score adjusted for only Raven's Progressive Matrices

An anonymous reviewer asked whether correcting reading score only for Raven's Progressive Matrices score (and not for digit span) would result in a different coefficient estimate for adjusted reading score in the recognition memory model. To answer the reviewer's question, we repeat the adjustment procedure with Raven's score, but without digit span score.

In [22]:
mixed_task_ravens = Model(df_bernoulli)
mixed_task_ravens.fit('score_bernoulli ~ task_z*ravens_z',
                      random=['1|pp'],
                      **defaults)
Auto-assigning NUTS sampler...
Initializing NUTS using advi+adapt_diag...
Average Loss = 7,945.6:  50%|█████     | 25099/50000 [01:14<01:14, 334.72it/s]
Convergence achieved at 25100
Interrupted at 25,099 [50%]: Average Loss = 8,823.2
Multiprocess sampling (4 chains in 4 jobs)
NUTS: [1|pp_offset, 1|pp_sd, task_z:ravens_z, ravens_z, task_z, Intercept]
Sampling 4 chains, 0 divergences: 100%|██████████| 30000/30000 [17:29<00:00, 28.60draws/s]
The acceptance probability does not match the target. It is 0.6886048552866366, but should be close to 0.8. Try to increase the number of tuning steps.
The number of effective samples is smaller than 10% for some parameters.
Out[22]:
<bambi.results.MCMCResults at 0x14337c208>
In [23]:
pps = df_reading['pp'].unique()
pp_nums = [f'1|pp[{i}]' for i in range(len(pps))]

df_intercepts = pm.summary(mixed_task_ravens.backend.trace).loc[pp_nums]
df_intercepts['pp'] = np.sort(pps)

display(df_intercepts.head().round(2))
mean sd hpd_3% hpd_97% mcse_mean mcse_sd ess_mean ess_sd ess_bulk ess_tail r_hat pp
1|pp[0] -0.28 0.31 -0.88 0.30 0.01 0.01 1402.0 1402.0 1402.0 3554.0 1.0 low_1
1|pp[1] 2.22 0.27 1.72 2.73 0.01 0.01 1031.0 1031.0 1029.0 2742.0 1.0 low_10
1|pp[2] 1.31 0.27 0.82 1.82 0.01 0.01 1077.0 1077.0 1074.0 2614.0 1.0 low_11
1|pp[3] 0.45 0.29 -0.09 0.99 0.01 0.01 1125.0 1125.0 1124.0 2882.0 1.0 low_12
1|pp[4] 1.51 0.30 0.94 2.09 0.01 0.01 1133.0 1133.0 1133.0 2829.0 1.0 low_13
In [24]:
df_uncorrected = df_reading.groupby('pp', as_index=False).mean().rename(columns={'score': 'raw_reading_score'})
df_intercepts = df_intercepts[['pp', 'mean']].rename(columns={'mean': 'adjusted_reading_score'})
df_intercepts = df_intercepts.merge(df_uncorrected,
                                    left_on='pp', right_on='pp').reset_index()

display(df_intercepts.head().round(2))
index pp adjusted_reading_score subject raw_ravens_mean ravens_intercept raw_span_mean span_intercept raw_reading_score task_z ravens_z span_z
0 0 low_1 -0.28 1.0 0.28 0.34 4.5 0.47 12.5 0.0 0.39 0.81
1 1 low_10 2.22 40.0 0.17 -0.17 3.5 0.02 45.5 0.0 -0.31 0.04
2 2 low_11 1.31 41.0 0.22 0.10 4.5 0.47 32.5 0.0 0.04 0.81
3 3 low_12 0.45 42.0 0.28 0.34 4.0 0.25 22.5 0.0 0.39 0.43
4 4 low_13 1.51 43.0 0.11 -0.46 5.0 0.69 23.5 0.0 -0.65 1.20
In [25]:
# write intercepts to file
df_intercepts.to_csv('data/reading_intercepts_adjusted_for_only_ravens.tsv', sep='\t')